package com.zeyu.framework.core.configuration;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.zeyu.framework.core.common.Constant;
import com.zeyu.framework.core.common.condition.DistributedCondition;
import com.zeyu.framework.core.distributed.RedisSessionDAO;
import com.zeyu.framework.core.distributed.ShiroSessionFactory;
import com.zeyu.framework.core.distributed.ShiroSessionListener;
import com.zeyu.framework.core.security.FormAuthenticationFilter;
import com.zeyu.framework.core.security.SystemAuthorizingRealm;
import com.zeyu.framework.core.security.UserFilter;
import com.zeyu.framework.core.security.session.CacheSessionDAO;
import com.zeyu.framework.core.security.session.SessionDAO;
import com.zeyu.framework.core.security.session.SessionManager;
import com.zeyu.framework.utils.IdGen;
import com.zeyu.framework.utils.OrderProperties;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import javax.servlet.Filter;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * shiro configuration
 * Created by zeyuphoenix on 16/6/25.
 */
@Configuration
public class ShiroConfiguration implements EnvironmentAware, Constant {

    // ================================================================
    // Constants
    // ================================================================

    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);

    private static final String LOGIN_URL = "/login";

    // ================================================================
    // Fields
    // ================================================================

    // 读取配置信息
    private RelaxedPropertyResolver propertyResolver;
    // 分布式
    private boolean distributed = false;

    // ================================================================
    // Constructors
    // ================================================================

    // ================================================================
    // Methods from/for super Interfaces or SuperClass
    // ================================================================

    @Override
    public void setEnvironment(Environment environment) {
        this.propertyResolver = new RelaxedPropertyResolver(environment,
                CONFIGURATION_DEFINED_PREFIX);
        // 初始是否为分布式
        this.distributed = Boolean.valueOf(this.propertyResolver.getProperty("distributed", "false"));
    }

    // ================================================================
    // Public or Protected Methods
    // ================================================================

    /**
     * Shiro权限过滤过滤器定义
     * Shiro连接约束配置,即过滤链的定义
     * 下面value值的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值
     * anon：它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种
     * authc：该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器
     * org.apache.shiro.web.filter.authc.FormAuthenticationFilter
     */
    @Bean(name = "shiroFilterChainDefinitions")
    public Map<String, String> shiroFilterChainDefinitionMap() {
        Map<String, String> shiroFilterChainDefinitionMap = Maps.newLinkedHashMap();

        try {
            OrderProperties properties = new OrderProperties();
            Resource resource = new ClassPathResource("shiro_filter_chain.properties");
            properties.load(resource.getInputStream());

            List<?> commentOrEntrys = properties.getContext().getCommentOrEntrys();

            commentOrEntrys.stream()
                    .filter(k -> k instanceof OrderProperties.PropertiesContext.PropertyEntry)
                    .forEach(k -> {
                        OrderProperties.PropertiesContext.PropertyEntry ent = (OrderProperties.PropertiesContext.PropertyEntry) k;
                        shiroFilterChainDefinitionMap.put(ent.getKey(), ent.getValue());
                    });

        } catch (IOException e) {
            logger.error("load shiro chain error: ", e);
        }

        return shiroFilterChainDefinitionMap;
    }

    /**
     * 定义Shiro安全管理配置
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(systemAuthorizingRealm());
        // 可选项 最好使用,SessionDao,中 doReadSession 读取过于频繁
        securityManager.setCacheManager(shiroCacheManager());
        // 可选项 默认使用ServletContainerSessionManager，直接使用容器的HttpSession，
        // 可以通过配置sessionManager，使用DefaultWebSessionManager来替代
        securityManager.setSessionManager(sessionManager());
        securityManager.setRememberMeManager(rememberManager());

        return securityManager;
    }

    /**
     * 安全认证过滤器
     * Shiro主过滤器本身功能十分强大,其强大之处就在于它支持任何基于URL路径表达式的、自定义的过滤器的执行
     * Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截,Shiro对基于Spring的Web应用提供了完美的支持
     * <p>
     * ShiroFilterFactoryBean实现了BeanPostProcessor接口：BeanPostProcessor是Ioc容器Bean管理的扩展点，
     * 定义了Bean实例化前后的回调方法,它比普通的bean初始化都要早,所以这里问题是调用其他@Configuration的bean可能没
     * 有初始化.
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter() {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        // Shiro的核心安全接口,这个属性是必须的
        shiroFilter.setSecurityManager(securityManager());
        // loginUrl ${cas.server.url}?service=${cas.project.url}/cas
        // 要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilter.setLoginUrl(LOGIN_URL);
        // 登录成功后要跳转的连接
        // shiroFilter.setSuccessUrl("?login");
        Map<String, Filter> filters = Maps.newLinkedHashMap();
        // filters.put("cas", casFilter());
        filters.put("authc", formAuthenticationFilter());
        filters.put("user", userFilter());
        shiroFilter.setFilters(filters);
        shiroFilter.setFilterChainDefinitionMap(shiroFilterChainDefinitionMap());

        return shiroFilter;
    }


    /**
     * 表单验证过滤类,继承shiro,添加自定义属性
     */
    //@Bean
    // 这里会自动注册filter到filter chain, 而我的filter只是shiro的控制,不作为全局,如果需要使用这个bean,需要FilterRegistrationBean
    public FormAuthenticationFilter formAuthenticationFilter() {
        return new FormAuthenticationFilter();
    }

    /**
     * 用户认证filter
     */
    //@Bean
    // 这里会自动注册filter到filter chain, 而我的filter只是shiro的控制,不作为全局,如果需要使用这个bean,需要FilterRegistrationBean
    public UserFilter userFilter() {
        return new UserFilter();
    }

    /**
     * 系统安全认证实现类
     * 自己需要实现的realm实现类 充当shiro和应用的安全数据的桥梁
     */
    @Bean
    public SystemAuthorizingRealm systemAuthorizingRealm() {
        return new SystemAuthorizingRealm();
    }

    /**
     * 生成唯一性ID算法.
     */
    @Bean
    public IdGen idGen() {
        return new IdGen();
    }

    /**
     * 自定义session管理配置
     * SessionManager默认是 ServletContainerSessionManager
     * 当使用ServletContainerSessionManager时，验证登陆返回值AuthenticationInfo中的Principal会被保存到HttpSession中，
     * 最后构造的shiro.session接口实现类中一个属性为HttpSession
     * <p>
     * 如果使用DefaultWebSessionManager时，会用到SessionDAO，默认的实现是MemorySessionDAO
     * （在其父类DefaultSessionManager中可以看到），如果使用MemorySessionDAO,Principal信息会被保存到MemorySessionDAO
     * 维护的一个 ConcurrentMap<Serializable, Session> sessions 中，如果要自己实现中央缓存，
     * 就重写该一个AbstractSessionDAO的子类并实现相关方法
     */
    @Bean(name = "sessionManager")
    public SessionManager sessionManager() {
        SessionManager sessionManager = new SessionManager();
        // 设置属性
        // 会话超时时间，单位：毫秒  ${session.sessionTimeout}
        sessionManager.setGlobalSessionTimeout(1800 * 1000L);
        // 定时清理失效会话,清理用户直接关闭浏览器造成的孤立会话 ${session.sessionTimeoutClean}
        sessionManager.setSessionValidationInterval(1800 * 1000L);
        // sessionManager.setSessionValidationScheduler(getExecutorServiceSessionValidationScheduler());

        // 默认JSESSIONID，同tomcat/jetty在cookie中缓存标识相同，修改用于防止访问404页面时，容器生成的标识把shiro的覆盖掉
        sessionManager.setSessionIdCookie(sessionIdCookie());
        sessionManager.setSessionIdCookieEnabled(true);
        // session存储器
        sessionManager.setSessionDAO(cachingShiroSessionDao());
        if (this.distributed) {
            // 是否开启会话验证器任务 默认true
            sessionManager.setSessionValidationSchedulerEnabled(false);
            // 是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true
            sessionManager.setDeleteInvalidSessions(false);

            sessionManager.setSessionFactory(sessionFactory());
            // 可以添加session 创建、删除的监听器
            List<SessionListener> listeners = Lists.newArrayList();
            listeners.add(shiroSessionListener());
            sessionManager.setSessionListeners(listeners);
        }

        return sessionManager;
    }

    /**
     * 自定义session存储器
     * 普通持久化接口，不会被缓存
     */
    /*@Bean(name = "sessionDAO")
     public SessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();

        // 设置属性
        redisSessionDAO.setSessionIdGenerator(idGen);
        redisSessionDAO.setSessionKeyPrefix("${redis.keyPrefix}_session_");

        return redisSessionDAO;
    }*/

    /**
     * 自定义session存储器
     * 可缓存Dao，操作自定义Session,添加标识位，减少doUpdate方法中Redis的连接次数来减轻网络压力
     */
    @Bean(name = "sessionDAO")
    public SessionDAO cachingShiroSessionDao() {

        // 本地使用可被缓存的Dao ，本地缓存减轻网络压力
        if (distributed) {
            // 分布式缓存
            RedisSessionDAO redisSessionDAO = new RedisSessionDAO();

            // 注意中央缓存有效时间要比本地缓存有效时间长
            redisSessionDAO.setSeconds(1800);
            // 特殊配置 只用于没有Redis时 将Session放到EhCache中
            redisSessionDAO.setOnlyEhCache(false);
            //redisSessionDAO.setSessionKeyPrefix("");

            return redisSessionDAO;
        } else {
            CacheSessionDAO cacheSessionDAO = new CacheSessionDAO();

            // 设置属性
            cacheSessionDAO.setSessionIdGenerator(idGen());
            cacheSessionDAO.setActiveSessionsCacheName("activeSessionsCache");
            cacheSessionDAO.setCacheManager(shiroCacheManager());

            return cacheSessionDAO;
        }
    }

    /**
     * 自定义Session工厂方法 返回会标识是否修改主要字段的自定义Session
     */
    @Bean
    public ShiroSessionFactory sessionFactory() {
        return new ShiroSessionFactory();
    }

    /**
     * Session没有从Redis中销毁，虽然当前重新new了一个，但会对统计带来干扰，通过SessionListener解决
     */
    @Bean
    @Conditional(DistributedCondition.class)
    public ShiroSessionListener shiroSessionListener() {
        return new ShiroSessionListener();
    }

    /**
     * CAS认证过滤器
     */
    //@Bean(name = "casFilter")
    // 这里会自动注册filter到filter chain, 而我的filter只是shiro的控制,不作为全局,如果需要使用这个bean,需要FilterRegistrationBean
    // 官方推荐buji-pac4j替换,新版本CasFilter已经过期,这里我们暂时不替换了
    /*public CasFilter casFilter() {
        CasFilter casFilter = new CasFilter();
        casFilter.setEnabled(true);
        casFilter.setName("cas");
        casFilter.setFailureUrl(LOGIN_URL);
        return casFilter;
    }*/

    /**
     * 指定本系统SESSIONID, 默认为: JSESSIONID
     * 问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT 等默认JSESSIONID,
     * 当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失!
     */
    @Bean(name = "sessionIdCookie")
    public SimpleCookie sessionIdCookie() {
        SimpleCookie simpleCookie = new SimpleCookie("framework.session.id");
        simpleCookie.setHttpOnly(true);
        // maxAge=-1表示浏览器关闭时失效此Cookie
        simpleCookie.setMaxAge(-1);
        return simpleCookie;
    }

    /**
     * remember cookie
     */
    @Bean(name = "rememberMeCookie")
    public SimpleCookie rememberMeCookie() {
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        simpleCookie.setHttpOnly(true);
        // 记住我的Cookie，保存时长30天
        simpleCookie.setMaxAge(SESSION_DEFAULT_AGE);
        return simpleCookie;
    }

    /**
     * remember manager
     */
    @Bean
    public CookieRememberMeManager rememberManager() {
        CookieRememberMeManager meManager = new CookieRememberMeManager();
        // cipherKey是加密rememberMe Cookie的密钥；默认AES算法
        meManager.setCipherKey(Base64.decode(COOKIE_CIPHER_KEY));
        meManager.setCookie(rememberMeCookie());
        return meManager;
    }

    /**
     * 这里可以自己实现一个CacheManage,例如基于shiro的session或者基于redis:
     * 1.implements CacheManager(注意是shiro的)
     * 2.实现接口方法,返回一个Cache(shiro的)
     * 3.创建一个类 implements Cache
     * 4.实现接口的方法
     */
    /*@Bean(name = "shiroCacheManager")
    public RedisCacheManager shiroRedisCacheManager() {
        return new RedisCacheManager();
    }*/
    /*@Bean(name = "sessionCacheManager")
    public SessionCacheManager sessionCacheManager() {
        return new SessionCacheManager();
    }*/

    /**
     * 定义缓存管理器, 采用ehcache实现
     * cacheManager的作用是保存doGetAuthorizationInfo（权限验证）中的返回结果，
     * 如果没有配置cacheManager，每一次权限认证都需要重新调用该方法
     *
     * @return shiro cache manager
     */
    @Bean(name = "shiroCacheManager")
    @DependsOn("lifecycleBeanPostProcessor")
    public EhCacheManager shiroCacheManager() {
        EhCacheManager ehCacheManager = new EhCacheManager();
        // 共用ehcache的缓存
        ehCacheManager.setCacheManager(ehcacheCacheManager().getCacheManager());

        return ehCacheManager;
    }

    /**
     * ehcache管理器
     */
    @Bean(name = "ehcacheCacheManager")
    public EhCacheCacheManager ehcacheCacheManager() {
        EhCacheCacheManager cacheManager = new EhCacheCacheManager();
        cacheManager.setCacheManager(ehCacheManagerFactoryBean().getObject());
        return cacheManager;
    }

    /**
     * 根据shared与否的设置,Spring通过new 和 create() 的方式创建manager
     */
    @Bean(name = "ehCacheManagerFactoryBean")
    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
        EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean();
        cacheManagerFactoryBean.setConfigLocation(new ClassPathResource(
                this.propertyResolver.getProperty("ehcache.config", "cache/ehcache.xml")));
        cacheManagerFactoryBean.setShared(true);

        return cacheManagerFactoryBean;
    }

    /**
     * 保证实现了Shiro内部lifecycle函数的bean执行,Shiro生命周期处理器
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * AOP式方法级权限检查,支持shiro对Controller的方法级APO安全控制
     * 开启Shiro的注解(如@RequiresRoles, @RequiresPermissions)
     * 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启Shiro的注解
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor
                = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

    // ================================================================
    // Getter & Setter
    // ================================================================

    // ================================================================
    // Private Methods
    // ================================================================

    // ================================================================
    // Inner or Anonymous Class
    // ================================================================

    // ================================================================
    // Test Methods
    // ================================================================

}
